Add share-intent target API for watchapps#171
Conversation
Lets watchapps register as Android share targets so users can "Share to
<watchapp>" from any other app on their phone. The shared payload (text,
URL, or both) routes to the watchapp's PKJS via a new `'shareintent'`
event listener.
This is a platform capability that benefits any watchapp accepting
external input — navigation apps receiving Maps URLs, music apps
receiving Spotify links, note-taking apps receiving selected text, etc.
The motivating use case is MirrorMap (a Pebble nav watchapp), where
sharing a Google Maps URL into the watchapp is the primary user flow,
but nothing in this PR is Maps-specific.
## What's new
### Watchapp side
Watchapps opt in via two new fields in `package.json`:
```json
{
"shareTarget": {
"mimeTypes": ["text/plain"]
}
}
```
PKJS receives shared payloads via:
```javascript
Pebble.addEventListener('shareintent', function(e) {
// e.text — the shared text/URL
});
```
### Android app side
Three Android share-sheet surfacing strategies, layered for compatibility:
1. **Static manifest entry** ("Pebble") — universal fallback, works on every
share-sheet implementation
2. **Sharing Shortcuts** with Direct Share metadata — surfaces individual
watchapps as named entries on Android 11+ where supported
3. **ChooserTargetService** compat — same goal for older / Samsung-style
share sheets where Sharing Shortcuts don't surface
When the static "Pebble" path is taken and multiple watchapps subscribe to
the shared MIME type, the user is presented with a chooser dialog to pick
the destination watchapp. When only one watchapp subscribes, dispatch is
direct (no extra tap).
### Per-watchapp icons in the share sheet
Each watchapp's share-target shortcut renders with the watchapp's own icon
extracted from its installed PBW. This required a small `.pbpack` resource
pack parser (`PbwResourcePack.kt`) since Pebble watchapps use a custom
binary format that bundles all resources into a single file. The parser
walks the manifest and resource table to extract the menu icon (declared
with `menuIcon: true` in `package.json`'s resources array) as a raw PNG,
then falls back to scanning the PBW zip root for any PNG if the resource
pack lookup fails.
The extracted icons are tinted via ColorMatrix to a brand-cohesive
appearance for share-target presentation while preserving alpha for
anti-aliasing on rounded backgrounds.
### Java-side short URL resolver
Many sharing flows generate short URLs that redirect to the actual content
URL (Google Maps' `maps.app.goo.gl`, Twitter's `t.co`, etc.). PKJS can in
principle resolve these via XHR, but in practice many short URL services
employ aggressive anti-bot measures that block XMLHttpRequest's User-Agent.
`ShareUrlResolver` runs the redirect resolution on the Android side using
the existing OkHttp/Ktor stack with a standard browser-like User-Agent.
The resolved long URL is what gets delivered to PKJS via the `shareintent`
event, so watchapp authors don't have to re-implement HTTP redirect
following in JavaScript or work around bot-detection headers.
The resolver:
- Has structured concurrency (cancels cleanly with the share dispatch)
- Retries once on transient network failure
- Uses a dedicated HttpClient instance so its configuration doesn't leak
into other libpebble3 HTTP usage
- Times out at 10s — beyond that the share fails gracefully
Currently configured for Google Maps short URL hosts; extending to other
short URL services is a one-line addition.
## Cold-start handling
Share intents commonly arrive while the target watchapp is not currently
running on the watch. The dispatch path:
1. Receives the share intent via the new manifest entry / shortcut
2. Looks up the subscribed watchapp(s) by MIME type
3. Asks the libpebble3 connection to launch the watchapp on the watch
4. Waits for PKJS to signal "ready" (CMD_INIT round-trip complete)
5. Delivers the share event
The launch + ready-signal phase has a 30s timeout for cold-starts where
the watchapp PBW needs to be transferred fresh, with a sub-timeout on the
producer flow for the initial state read.
## Testing notes
End-to-end validated on a Samsung Galaxy + Pebble Time Steel with MirrorMap
as the share target. Tested:
- Cold-start (watchapp not running, app not in foreground) — share intent
triggers watchapp launch, share is delivered after ready signal
- Warm-start (watchapp foreground) — share is delivered without relaunch
- Multi-watchapp scenario — chooser dialog surfaces correctly
- Per-watchapp icons render with their actual PBW menu icon
- Short URL resolution — `maps.app.goo.gl` URLs resolve to the long Maps
directions URL before PKJS sees them
## Files
New:
- `composeApp/src/androidMain/kotlin/coredevices/coreapp/sharing/ShareTargetActivity.kt`
- `composeApp/src/androidMain/res/xml/share_targets.xml`
- `libpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/shareintent/ShareTargetSync.kt`
- `libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/di/ShareIntentModule.kt`
- `libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/disk/pbw/PbwResourcePack.kt`
- `libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/shareintent/ShareIntentDispatcher.kt`
- `libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/shareintent/ShareTargetEntry.kt`
- `libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/shareintent/ShareTargetsProducer.kt`
- `libpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/shareintent/ShareUrlResolver.kt`
Modified:
- `composeApp/build.gradle.kts` — Direct Share / shortcut metadata deps
- `composeApp/src/androidMain/AndroidManifest.xml` — share-target manifest entry
- `composeApp/src/androidMain/kotlin/coredevices/coreapp/MainApplication.kt` — bootstrap
- `composeApp/src/androidMain/kotlin/coredevices/coreapp/di/androidDefaultModule.kt` — koin
- `gradle/libs.versions.toml` — version catalog updates
- `libpebble3/src/androidMain/assets/startup.js` — PKJS event registration
- `libpebble3/src/androidMain/kotlin/.../js/WebViewJsRunner.kt` — event dispatch
- `libpebble3/src/commonMain/kotlin/.../di/LibPebbleModule.kt` — koin
- `libpebble3/src/commonMain/kotlin/.../disk/pbw/DiskUtil.kt` — resource pack helpers
- `libpebble3/src/commonMain/kotlin/.../js/JsRunner.kt` — event interface
- `libpebble3/src/commonMain/kotlin/.../js/PKJSApp.kt` — ready signal observation
- `libpebble3/src/commonMain/kotlin/.../metadata/pbw/appinfo/PbwAppInfo.kt` — shareTarget field
- `libpebble3/src/iosMain/kotlin/.../js/JavascriptCoreJsRunner.kt` — iOS no-op stub
0fceba2 to
ff3b04b
Compare
Replaces the 2-attempt 300ms-fixed-delay retry in ShareUrlResolver with a 4-attempt exponential schedule (0, 2s, 4s, 6s) with +/-200ms symmetric jitter. Motivation: field testing showed Google's short-URL service returning 404 stochastically -- both attempts in the previous 2-attempt loop hit 404 within ~150ms of each other, falling open to PKJS's XHR fallback which then hits 403 (Google's anti-bot hits XHR harder than OkHttp). Spreading attempts across ~13s with jitter avoids the regular- interval polling pattern that contributes to the bot heuristic, while still bounded by a 16s overall timeout. Happy path stays snappy (attempt 1 immediate +/-200ms jitter).
ff3b04b to
f82d303
Compare
|
I'm not convinced this belongs in the Pebble app vs in an Android companion app. PKJS is supposed to be a cross-platform framework but this is completely android-only. There's also a lot of code in here which looks very specific to the Maps use-case. I didn't fully review but it looks like there are a lot of hacks (increasing PKJS complexity in a way I don't like the look of), and generally an abundance of code (and comments). |
|
might be my fault - I thought this might be a more generalizable path (eg sharing an image with a watchface from your photo album) |
|
Thanks for that info, @sjp4 -- I appreciate you taking the time to analyze all this especially coming from a complete stranger. @ericmigi — your photo-album-to-watchface example is what I was thinking too-- Things like this:
Each of those is one watchapp + one PKJS event handler away from working, with no platform changes needed beyond what's in this PR. The MIME-type filter in package.json means watchapps can declare what they accept (text, image, etc.), and Android share-sheet routing handles the rest. Sharing to pebble apps would really put an end to many companion app needs, making the user experience less fragmented. @sjp4 — your concerns are fair, especially on the Maps-specificity. ShareUrlResolver is genuinely Maps-specific and shouldn't be in libpebble3. The motivation was that PKJS XHR can't override User-Agent, and Google's maps.app.goo.gl URLs return 403 to the default UA — but that's my problem to solve, not the platform's. If we continue on this, I'll move URL resolution to a small backend endpoint of mine and drop ShareUrlResolver from the PR entirely. The comments and code volume — fair. Some of the bulk is necessary plumbing (Direct Share + ChooserTargetService compat is genuinely two paths because Android changed the API; PbwResourcePack exists because watchapp icons live in a custom binary format). But comment density is on me — I tend to over-document while iterating, and that needs trimming before review. The cross-platform concern — iOS does support an equivalent capability via Share Extensions (UIActivityViewController). The PKJS API I'm adding (the shareTarget package.json field, the shareintent event) is platform-neutral; only the implementation in this PR is Android. I'd be glad to scope a follow-up PR for the iOS Share Extension to keep parity. Same shape as PR #172's notificationFilter — unified PKJS API, OS-mechanic-specific implementations underneath. (MirrorMap aims to be cross-platform post-launch, so I'm motivated to do that work either way.) If you are open to giving me another shot, I can do either: A: Push a revision to this PR (drop ShareUrlResolver, prune comments, tighten the diff), or B: Close this PR and open a smaller, cleaner one with just the generic primitive (share intent → PKJS event), with per-watchapp icon extraction and the iOS implementation as separate follow-ups. I'm fine with either — whichever is easier for you to review. |
|
Just learned there are high res versions of app icons already available for the watch apps that users have installed. @sjp4 |
Lets watchapps register as Android share targets so users can "Share to " from any other app on their phone. The shared payload (text, URL, or both) routes to the watchapp's PKJS via a new
'shareintent'event listener.This is a platform capability that benefits any watchapp accepting external input — navigation apps receiving Maps URLs, music apps receiving Spotify links, note-taking apps receiving selected text, etc. The motivating use case is MirrorMap (a Pebble nav watchapp), where sharing a Google Maps URL into the watchapp is the primary user flow, but nothing in this PR is Maps-specific.
What's new
Watchapp side
Watchapps opt in via two new fields in
package.json:{ "shareTarget": { "mimeTypes": ["text/plain"] } }PKJS receives shared payloads via:
Android app side
Three Android share-sheet surfacing strategies, layered for compatibility:
When the static "Pebble" path is taken and multiple watchapps subscribe to the shared MIME type, the user is presented with a chooser dialog to pick the destination watchapp. When only one watchapp subscribes, dispatch is direct (no extra tap).
Per-watchapp icons in the share sheet
Each watchapp's share-target shortcut renders with the watchapp's own icon extracted from its installed PBW. This required a small
.pbpackresource pack parser (PbwResourcePack.kt) since Pebble watchapps use a custom binary format that bundles all resources into a single file. The parser walks the manifest and resource table to extract the menu icon (declared withmenuIcon: trueinpackage.json's resources array) as a raw PNG, then falls back to scanning the PBW zip root for any PNG if the resource pack lookup fails.The extracted icons are tinted via ColorMatrix to a brand-cohesive appearance for share-target presentation while preserving alpha for anti-aliasing on rounded backgrounds.
Java-side short URL resolver
Many sharing flows generate short URLs that redirect to the actual content URL (Google Maps'
maps.app.goo.gl, Twitter'st.co, etc.). PKJS can in principle resolve these via XHR, but in practice many short URL services employ aggressive anti-bot measures that block XMLHttpRequest's User-Agent.ShareUrlResolverruns the redirect resolution on the Android side using the existing OkHttp/Ktor stack with a standard browser-like User-Agent. The resolved long URL is what gets delivered to PKJS via theshareintentevent, so watchapp authors don't have to re-implement HTTP redirect following in JavaScript or work around bot-detection headers.The resolver:
Currently configured for Google Maps short URL hosts; extending to other short URL services is a one-line addition.
Cold-start handling
Share intents commonly arrive while the target watchapp is not currently running on the watch. The dispatch path:
The launch + ready-signal phase has a 30s timeout for cold-starts where the watchapp PBW needs to be transferred fresh, with a sub-timeout on the producer flow for the initial state read.
Testing notes
End-to-end validated on a Samsung Galaxy + Pebble Time Steel with MirrorMap as the share target. Tested:
maps.app.goo.glURLs resolve to the long Maps directions URL before PKJS sees themFiles
New:
composeApp/src/androidMain/kotlin/coredevices/coreapp/sharing/ShareTargetActivity.ktcomposeApp/src/androidMain/res/xml/share_targets.xmllibpebble3/src/androidMain/kotlin/io/rebble/libpebblecommon/shareintent/ShareTargetSync.ktlibpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/di/ShareIntentModule.ktlibpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/disk/pbw/PbwResourcePack.ktlibpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/shareintent/ShareIntentDispatcher.ktlibpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/shareintent/ShareTargetEntry.ktlibpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/shareintent/ShareTargetsProducer.ktlibpebble3/src/commonMain/kotlin/io/rebble/libpebblecommon/shareintent/ShareUrlResolver.ktModified:
composeApp/build.gradle.kts— Direct Share / shortcut metadata depscomposeApp/src/androidMain/AndroidManifest.xml— share-target manifest entrycomposeApp/src/androidMain/kotlin/coredevices/coreapp/MainApplication.kt— bootstrapcomposeApp/src/androidMain/kotlin/coredevices/coreapp/di/androidDefaultModule.kt— koingradle/libs.versions.toml— version catalog updateslibpebble3/src/androidMain/assets/startup.js— PKJS event registrationlibpebble3/src/androidMain/kotlin/.../js/WebViewJsRunner.kt— event dispatchlibpebble3/src/commonMain/kotlin/.../di/LibPebbleModule.kt— koinlibpebble3/src/commonMain/kotlin/.../disk/pbw/DiskUtil.kt— resource pack helperslibpebble3/src/commonMain/kotlin/.../js/JsRunner.kt— event interfacelibpebble3/src/commonMain/kotlin/.../js/PKJSApp.kt— ready signal observationlibpebble3/src/commonMain/kotlin/.../metadata/pbw/appinfo/PbwAppInfo.kt— shareTarget fieldlibpebble3/src/iosMain/kotlin/.../js/JavascriptCoreJsRunner.kt— iOS no-op stub